
public class Block {
	public static final int blockSize = 4;
	private int keyLength, rounds;
	private Word [] block = new Word [4], key, encrypted, decrypted;
	
	//-------------------------------------------
	public Block(String phrase, String key) {
		for(int i = 0; i < blockSize; i++) {
			block[i] = new Word(phrase.substring(i*8, i*8+8));
		}
		
		keyLength = key.length() / 8;
		switch(keyLength) {
			case 4:
				rounds = 10;
				break;
			case 6:
				rounds = 12;
				break;
			case 8:
				rounds = 14;
				break;
			default:
				System.out.println("The key was not of a valid length");	
		}
		
		this.key = new Word [keyLength];
		for(int i = 0; i < keyLength; i++) {
			this.key[i] = new Word(key.substring(i*8, i*8+8));
		}
	}
	
	//-------------------------------------------
	public String cipher() {
		Word roundKey [] = expandKey();
		
		encrypted = new Word [4];
		for(int i = 0; i < 4; i++) {
			encrypted[i] = new Word(block[i]);
		}
		
		print("Input: ", encrypted);
		addRoundKey(roundKey, 0);
		for(int i = 1; i <= rounds - 1; i++) {
//			System.out.println("----------------------------------\nRound " + i + ":");
//			print("Begin: ", encrypted);
			subBytes();
//			print("after subBytes: ", encrypted);
			shiftRows();
//			print("after shiftRows: ", encrypted);
			mixColumns();
//			print("after MixColumns: ", encrypted);
			addRoundKey(roundKey, i);
//			print("after RoundKey: ", encrypted);
		}
		
		subBytes();
		shiftRows();
		addRoundKey(roundKey, rounds);
		
		String result = "";
		for(int i = 0; i < encrypted.length; i++) {
			result += encrypted[i];
		}
		print("Encrypted: ", encrypted);
		return result;
	}
	
	//-------------------------------------------
	public String invCipher() {
		Word roundKey [] = expandKey();
		
		decrypted = new Word [4];
		for(int i = 0; i < 4; i++) {
			decrypted[i] = new Word(block[i]);
		}
		
		print("Input: ", decrypted);
//		print("round[10].iinput: ", decrypted);
		invAddRoundKey(roundKey, rounds);
//		print("round[10].ik_add: ", decrypted);
		for(int i = rounds - 1; i > 0; i--) {
//			System.out.println("----------------------------------\nRound " + i + ":");
			invShiftRows();
//			print("round[ " + i + "].is_row: ", decrypted);
			invSubBytes();
//			print("round[ " + i + "].is_box: ", decrypted);
			invAddRoundKey(roundKey, i);
//			print("round[ " + i + "].ik_add: ", decrypted);
			invMixColumns();
//			print("round[ " + i + "].istart: ", decrypted);
		}
		
		invShiftRows();
//		print("round[ " + 0 + "].is_row: ", decrypted);
		invSubBytes();
//		print("round[ " + 0 + "].ik_add: ", decrypted);
		invAddRoundKey(roundKey, 0);
//		print("round[ " + 0 + "].ik_add: ", decrypted);
		
		String result = "";
		for(int i = 0; i < decrypted.length; i++) {
			result += decrypted[i];
		}
		print("Decrypted: ", decrypted);
		return result;
	}
	
	//-------------------------------------------
	private Word ffAdd(Word f1, Word f2) {
		return new Word(f1.bytes[0]^f2.bytes[0], f1.bytes[1]^f2.bytes[1], 
						f1.bytes[2]^f2.bytes[2], f1.bytes[3]^f2.bytes[3]);
	}
	private int ffAdd(int f1, int f2) {
		return f1^f2;
	}
	
	//-------------------------------------------
	private int xTime(int f) {
		int result = f*2;
		if(result >= 0x100) {
			result = result^0x11B;
		}
		return result;
	}
	
	private int xTime(int f, int power) {
		int result = f;
		for(int i = 0; i < power; i++) {
			result = xTime(result);
		}
		return result;
	}
	
	//-------------------------------------------
	private int ffMultiply(int f1, int f2) {
		int result = 0, power = 0;
		while(f2 > 0) {
			if(f2 % 2 == 1) {
				result = ffAdd(result, xTime(f1, power));
			}
			power++;
			f2 /= 2;
		}
		
//		System.out.print("'" + Integer.toHexString(result) + "'");
		return result;
	}
	
	//-------------------------------------------
	private Word[] expandKey() {
		Word temp;
		Word w [] = new Word [blockSize * (rounds + 1)];
		int i=0;
		
		//Sets the key into the round schedule
		while (i < keyLength) {
			w[i] = new Word(key[i].bytes[0], key[i].bytes[1], key[i].bytes[2], key[i].bytes[3]);
			i++;
		}
		
		while (i < blockSize * (rounds + 1)) {
			temp = w[i-1];
			if (i % keyLength == 0)
				temp = ffAdd(subWord(rotWord(temp)), Word.Rcon[i/keyLength]);
			else if (keyLength > 6 && i % keyLength == 4)
				temp = subWord(temp);
			w[i] = ffAdd(w[i-keyLength], temp);
			i++;
		}
		return w;
	}
	
	//-------------------------------------------
	private Word subWord(Word w) {
		Word result = new Word(w);
		result.subBytesEncrypt();
		return result;
	}
	
	//-------------------------------------------
	private Word rotWord(Word w) {
		Word result = new Word(w);
		result.rotWord();
		return result;
	}
	
	//-------------------------------------------
	private void subBytes() {
		for(int i = 0; i < 4; i++) {
			encrypted[i].subBytesEncrypt();
		}
	}
	
	private void invSubBytes() {
		for(int i = 0; i < 4; i++) {
			decrypted[i].subBytesDecrypt();
		}
	}
	//-------------------------------------------
	private void shiftRows() {
		for(int i = 0; i < 4; i++) {
			for(int j = 0; j < i; j++) {
				shiftRowOnce(i);
			}
		}
	}
	
	private void invShiftRows() {
		for(int i = 0; i < 4; i++) {
			for(int j = 0; j < i; j++) {
				invShiftRowOnce(i);
			}
		}
	}
	
	private void shiftRowOnce(int index) {
		int temp = encrypted[0].bytes[index];
		encrypted[0].bytes[index] = encrypted[1].bytes[index];
		encrypted[1].bytes[index] = encrypted[2].bytes[index];
		encrypted[2].bytes[index] = encrypted[3].bytes[index];
		encrypted[3].bytes[index] = temp;
	}
	
	private void invShiftRowOnce(int index) {
		int temp = decrypted[3].bytes[index];
		decrypted[3].bytes[index] = decrypted[2].bytes[index];
		decrypted[2].bytes[index] = decrypted[1].bytes[index];
		decrypted[1].bytes[index] = decrypted[0].bytes[index];
		decrypted[0].bytes[index] = temp;
	}
	//-------------------------------------------
	private void mixColumns() {
		Word [] mixedColumns = new Word [4];
		for(int column = 0; column < 4; column++) {
			mixedColumns[column] = new Word();
			for(int row = 0; row < 4; row++) {
				int sTotal = 0;
				for(int cell = 0; cell < 4; cell++) {
					int s = encrypted[column].bytes[cell];
					if(row == cell) {
						sTotal = ffAdd(sTotal, ffMultiply(s, 2));
					}
					else if((row + 1) % 4 == cell) {
						sTotal = ffAdd(sTotal, ffMultiply(s, 3));
					}
					else {
						sTotal = ffAdd(sTotal, s);
					}
//					System.out.print(sTotal + " : ");
				}
//				System.out.println();
				mixedColumns[column].bytes[row] = sTotal;
			}
		}
		encrypted = mixedColumns;
	}
	
	private void invMixColumns() {
		Word [] mixedColumns = new Word [4];
		for(int column = 0; column < 4; column++) {
			mixedColumns[column] = new Word();
			for(int row = 0; row < 4; row++) {
				int sTotal = 0;
				for(int cell = 0; cell < 4; cell++) {
					int s = decrypted[column].bytes[cell];
					if(row == cell) {
						sTotal = ffAdd(sTotal, ffMultiply(s, 14));
					}
					else if((row + 1) % 4 == cell) {
						sTotal = ffAdd(sTotal, ffMultiply(s, 11));
					}
					else if((row + 2) % 4 == cell){
						sTotal = ffAdd(sTotal, ffMultiply(s, 13));
					}
					else {
						sTotal = ffAdd(sTotal, ffMultiply(s, 9));
					}
//					System.out.print(sTotal + " : ");
				}
//				System.out.println();
				mixedColumns[column].bytes[row] = sTotal;
			}
		}
		decrypted = mixedColumns;
	}
	
	//-------------------------------------------
	private void addRoundKey(Word [] roundKey, int round) {
		for(int i = 0; i < 4; i++) {
			encrypted[i] = ffAdd(encrypted[i], roundKey[(round) * 4 + i]);
		}
	}
	
	private void invAddRoundKey(Word [] roundKey, int round) {
//		print("round[ " + round + "].k_sch: ", new Word [] {roundKey[(round) * 4],roundKey[(round) * 4 + 1], 
//											roundKey[(round) * 4 + 2], roundKey[(round) * 4 + 3] });
		for(int i = 0; i < 4; i++) {
			decrypted[i] = ffAdd(decrypted[i], roundKey[(round) * 4 + i]);
		}
	}
	
	//-------------------------------------------
	private void print(String s, Word [] phrase) {
		System.out.print(s);
		for(int i = 0; i < phrase.length; i++) {
			System.out.print(phrase[i] + " ");
		}
		System.out.println();
	}
}
